package org.jradweb.core.orm.validator;

import java.beans.Introspector;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.AssertionFailure;
import org.hibernate.Hibernate;
import org.hibernate.MappingException;
import org.hibernate.cfg.BinderHelper;
import org.hibernate.cfg.annotations.Version;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.reflection.Filter;
import org.hibernate.reflection.ReflectionManager;
import org.hibernate.reflection.XClass;
import org.hibernate.reflection.XMember;
import org.hibernate.reflection.XMethod;
import org.hibernate.reflection.XProperty;
import org.hibernate.reflection.java.JavaXFactory;
import org.hibernate.util.IdentitySet;
import org.hibernate.validator.ClassValidator;
import org.hibernate.validator.InvalidStateException;
import org.hibernate.validator.InvalidValue;
import org.hibernate.validator.MessageInterpolator;
import org.hibernate.validator.PersistentClassConstraint;
import org.hibernate.validator.PropertyConstraint;
import org.hibernate.validator.Valid;
import org.hibernate.validator.Validator;
import org.hibernate.validator.ValidatorClass;
import org.hibernate.validator.interpolator.DefaultMessageInterpolatorAggerator;

public class JClassValidator<T> implements Serializable {
	private static Log log = LogFactory.getLog(JClassValidator.class);
	private static final InvalidValue[] EMPTY_INVALID_VALUE_ARRAY = new InvalidValue[] {};
	private static final String DEFAULT_VALIDATOR_MESSAGE = "org.hibernate.validator.resources.DefaultValidatorMessages";
	private static final String VALIDATOR_MESSAGE = "ValidatorMessages";
	private static final Set<Class> INDEXABLE_CLASS = new HashSet<Class>();

	static {
		INDEXABLE_CLASS.add(Integer.class);
		INDEXABLE_CLASS.add(Long.class);
		INDEXABLE_CLASS.add(String.class);
	}

	static {
		Version.touch(); // touch version
	}

	private final Class<T> beanClass;
	private transient ResourceBundle messageBundle;
	private transient ResourceBundle defaultMessageBundle;
	private transient boolean isUserProvidedResourceBundle;
	private transient ReflectionManager reflectionManager;

	private final transient Map<XClass, JClassValidator> childClassValidators;
	private transient List<Validator> beanValidators;
	private transient List<Validator> memberValidators;
	private transient List<XMember> memberGetters;
	private transient List<XMember> childGetters;
	private transient DefaultMessageInterpolatorAggerator defaultInterpolator;
	private transient MessageInterpolator userInterpolator;
	private static final Filter GET_ALL_FILTER = new Filter() {
		public boolean returnStatic() {
			return true;
		}

		public boolean returnTransient() {
			return true;
		}
	};

	/**
	 * create the validator engine for this bean type
	 */
	public JClassValidator(Class<T> beanClass) {
		this(beanClass, (ResourceBundle) null);
	}

	/**
	 * create the validator engine for a particular bean class, using a resource
	 * bundle for message rendering on violation
	 */
	public JClassValidator(Class<T> beanClass, ResourceBundle resourceBundle) {
		this(beanClass, resourceBundle, null,
				new HashMap<XClass, JClassValidator>(), null);
	}

	/**
	 * create the validator engine for a particular bean class, using a custom
	 * message interpolator for message rendering on violation
	 */
	public JClassValidator(Class<T> beanClass, MessageInterpolator interpolator) {
		this(beanClass, null, interpolator,
				new HashMap<XClass, JClassValidator>(), null);
	}

	/**
	 * Not a public API
	 */
	public JClassValidator(Class<T> beanClass, ResourceBundle resourceBundle,
			MessageInterpolator interpolator,
			Map<XClass, JClassValidator> childClassValidators,
			ReflectionManager reflectionManager) {
		this.reflectionManager = reflectionManager != null ? reflectionManager
				: new JavaXFactory();
		XClass beanXClass = this.reflectionManager.toXClass(beanClass);
		this.beanClass = beanClass;
		this.messageBundle = resourceBundle == null ? getDefaultResourceBundle()
				: resourceBundle;
		this.defaultMessageBundle = ResourceBundle
				.getBundle(DEFAULT_VALIDATOR_MESSAGE);
		this.userInterpolator = interpolator;
		this.childClassValidators = childClassValidators != null ? childClassValidators
				: new HashMap<XClass, JClassValidator>();
		initValidator(beanXClass, this.childClassValidators);
	}

	@SuppressWarnings("unchecked")
	protected JClassValidator(XClass beanXClass, ResourceBundle resourceBundle,
			MessageInterpolator userInterpolator,
			Map<XClass, JClassValidator> childClassValidators,
			ReflectionManager reflectionManager) {
		this.beanClass = reflectionManager.toClass(beanXClass);
		this.messageBundle = resourceBundle == null ? getDefaultResourceBundle()
				: resourceBundle;
		this.defaultMessageBundle = ResourceBundle
				.getBundle(DEFAULT_VALIDATOR_MESSAGE);
		this.userInterpolator = userInterpolator;
		this.childClassValidators = childClassValidators;
		initValidator(beanXClass, childClassValidators);
	}

	private ResourceBundle getDefaultResourceBundle() {
		ResourceBundle rb;
		try {
			// use context class loader as a first citizen
			ClassLoader contextClassLoader = Thread.currentThread()
					.getContextClassLoader();
			if (contextClassLoader == null) {
				throw new MissingResourceException("No context classloader",
						null, VALIDATOR_MESSAGE);
			}
			rb = ResourceBundle.getBundle(VALIDATOR_MESSAGE, Locale
					.getDefault(), contextClassLoader);
		} catch (MissingResourceException e) {
			log.trace("ResourceBundle " + VALIDATOR_MESSAGE
					+ " not found in thread context classloader");
			// then use the Validator Framework classloader
			try {
				rb = ResourceBundle.getBundle(VALIDATOR_MESSAGE, Locale
						.getDefault(), this.getClass().getClassLoader());
			} catch (MissingResourceException ee) {
				log
						.debug("ResourceBundle ValidatorMessages not found in Validator classloader. Delegate to "
								+ DEFAULT_VALIDATOR_MESSAGE);
				// the user did not override the default ValidatorMessages
				rb = null;
			}
		}
		isUserProvidedResourceBundle = true;
		return rb;
	}

	private void initValidator(XClass xClass,
			Map<XClass, JClassValidator> childClassValidators) {
		beanValidators = new ArrayList<Validator>();
		memberValidators = new ArrayList<Validator>();
		memberGetters = new ArrayList<XMember>();
		childGetters = new ArrayList<XMember>();
		defaultInterpolator = new DefaultMessageInterpolatorAggerator();
		defaultInterpolator.initialize(messageBundle, defaultMessageBundle);

		// build the class hierarchy to look for members in
		childClassValidators.put(xClass, this);
		Collection<XClass> classes = new HashSet<XClass>();
		addSuperClassesAndInterfaces(xClass, classes);
		for (XClass currentClass : classes) {
			Annotation[] classAnnotations = currentClass.getAnnotations();
			for (int i = 0; i < classAnnotations.length; i++) {
				Annotation classAnnotation = classAnnotations[i];
				Validator beanValidator = createValidator(classAnnotation);
				if (beanValidator != null)
					beanValidators.add(beanValidator);
			}
		}

		// Check on all selected classes
		for (XClass currClass : classes) {
			List<XMethod> methods = currClass.getDeclaredMethods();
			for (XMethod method : methods) {
				createMemberValidator(method);
				createChildValidator(method);
			}

			List<XProperty> fields = currClass.getDeclaredProperties("field",
					GET_ALL_FILTER);
			for (XProperty field : fields) {
				createMemberValidator(field);
				createChildValidator(field);
			}
		}
	}

	private void addSuperClassesAndInterfaces(XClass clazz,
			Collection<XClass> classes) {
		for (XClass currClass = clazz; currClass != null; currClass = currClass
				.getSuperclass()) {
			if (!classes.add(currClass))
				return;
			XClass[] interfaces = currClass.getInterfaces();
			for (XClass interf : interfaces) {
				addSuperClassesAndInterfaces(interf, classes);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private void createChildValidator(XMember member) {
		if (member.isAnnotationPresent(Valid.class)) {
			setAccessible(member);
			childGetters.add(member);
			XClass clazz;
			if (member.isCollection() || member.isArray()) {
				clazz = member.getElementClass();
			} else {
				clazz = member.getType();
			}
			if (!childClassValidators.containsKey(clazz)) {
				new JClassValidator(clazz, messageBundle, userInterpolator,
						childClassValidators, reflectionManager);
			}
		}
	}

	private void createMemberValidator(XMember member) {
		boolean validatorPresent = false;
		Annotation[] memberAnnotations = member.getAnnotations();
		for (int j = 0; j < memberAnnotations.length; j++) {
			Annotation methodAnnotation = memberAnnotations[j];
			Validator propertyValidator = createValidator(methodAnnotation);
			if (propertyValidator != null) {
				memberValidators.add(propertyValidator);
				setAccessible(member);
				memberGetters.add(member);
				validatorPresent = true;
			}
		}
		if (validatorPresent && !member.isTypeResolved()) {
			log.warn("Original type of property " + member
					+ " is unbound and has been approximated.");
		}
	}

	private static void setAccessible(XMember member) {
		if (!Modifier.isPublic(member.getModifiers())) {
			member.setAccessible(true);
		}
	}

	@SuppressWarnings("unchecked")
	private Validator createValidator(Annotation annotation) {
		try {
			ValidatorClass validatorClass = annotation.annotationType()
					.getAnnotation(ValidatorClass.class);
			if (validatorClass == null) {
				return null;
			}
			Validator beanValidator = validatorClass.value().newInstance();
			beanValidator.initialize(annotation);
			defaultInterpolator.addInterpolator(annotation, beanValidator);
			return beanValidator;
		} catch (Exception e) {
			throw new IllegalArgumentException(
					"could not instantiate ClassValidator", e);
		}
	}

	public boolean hasValidationRules() {
		return beanValidators.size() != 0 || memberValidators.size() != 0;
	}

	/**
	 * apply constraints on a bean instance and return all the failures. if
	 * <code>bean</code> is null, an empty array is returned
	 */
	public InvalidValue[] getInvalidValues(T bean) {
		return this.getInvalidValues(bean, new IdentitySet());
	}

	/**
	 * apply constraints on a bean instance and return all the failures. if
	 * <code>bean</code> is null, an empty array is returned
	 */
	@SuppressWarnings("unchecked")
	protected InvalidValue[] getInvalidValues(T bean,
			Set<Object> circularityState) {
		if (bean == null || circularityState.contains(bean)) {
			return EMPTY_INVALID_VALUE_ARRAY; // Avoid circularity
		} else {
			circularityState.add(bean);
		}

		if (!beanClass.isInstance(bean)) {
			throw new IllegalArgumentException("not an instance of: "
					+ bean.getClass());
		}

		List<InvalidValue> results = new ArrayList<InvalidValue>();

		for (int i = 0; i < beanValidators.size(); i++) {
			Validator validator = beanValidators.get(i);
			if (!validator.isValid(bean)) {
				if (validator instanceof BeanValidator) {
					BeanValidator beanValidator = (BeanValidator) validator;
					String propertyNames[] = beanValidator.getPropertyName();
					if (propertyNames == null || propertyNames.length == 0) {
						results.add(new InvalidValue(interpolate(validator),
								beanClass, null, bean, bean));
					} else {
						for (String propertyName : propertyNames) {
							results.add(new InvalidValue(
									interpolate(validator), beanClass,
									propertyName, bean,
									bean));
						}
					}
				} else {
					results.add(new InvalidValue(interpolate(validator),
							beanClass, null, bean, bean));
				}
			}
		}

		for (int i = 0; i < memberValidators.size(); i++) {
			XMember getter = memberGetters.get(i);
			if (Hibernate.isPropertyInitialized(bean, getPropertyName(getter))) {
				Object value = getMemberValue(bean, getter);
				Validator validator = memberValidators.get(i);
				if (!validator.isValid(value)) {
					String propertyName = getPropertyName(getter);
					results.add(new InvalidValue(interpolate(validator),
							beanClass, propertyName, value, bean));
				}
			}
		}

		for (int i = 0; i < childGetters.size(); i++) {
			XMember getter = childGetters.get(i);
			if (Hibernate.isPropertyInitialized(bean, getPropertyName(getter))) {
				Object value = getMemberValue(bean, getter);
				if (value != null && Hibernate.isInitialized(value)) {
					String propertyName = getPropertyName(getter);
					if (getter.isCollection()) {
						int index = 0;
						boolean isIterable = value instanceof Iterable;
						Map map = !isIterable ? (Map) value : null;
						Iterable elements = isIterable ? (Iterable) value : map
								.keySet();
						for (Object element : elements) {
							Object actualElement = isIterable ? element : map
									.get(element);
							if (actualElement == null) {
								index++;
								continue;
							}
							InvalidValue[] invalidValues = getClassValidator(
									actualElement).getInvalidValues(
									actualElement, circularityState);

							String indexedPropName = MessageFormat
									.format("{0}[{1}]", propertyName,
											INDEXABLE_CLASS.contains(element
													.getClass()) ? ("'"
													+ element + "'") : index);
							index++;

							for (InvalidValue invalidValue : invalidValues) {
								invalidValue.addParentBean(bean,
										indexedPropName);
								results.add(invalidValue);
							}
						}
					}
					if (getter.isArray()) {
						int index = 0;
						for (Object element : (Object[]) value) {
							if (element == null) {
								index++;
								continue;
							}
							InvalidValue[] invalidValues = getClassValidator(
									element).getInvalidValues(element,
									circularityState);

							String indexedPropName = MessageFormat.format(
									"{0}[{1}]", propertyName, index);
							index++;

							for (InvalidValue invalidValue : invalidValues) {
								invalidValue.addParentBean(bean,
										indexedPropName);
								results.add(invalidValue);
							}
						}
					} else {
						InvalidValue[] invalidValues = getClassValidator(value)
								.getInvalidValues(value, circularityState);
						for (InvalidValue invalidValue : invalidValues) {
							invalidValue.addParentBean(bean, propertyName);
							results.add(invalidValue);
						}
					}
				}
			}
		}

		return results.toArray(new InvalidValue[results.size()]);
	}

	private String interpolate(Validator validator) {
		String message = defaultInterpolator.getAnnotationMessage(validator);
		if (userInterpolator != null) {
			return userInterpolator.interpolate(message, validator,
					defaultInterpolator);
		} else {
			return defaultInterpolator.interpolate(message, validator, null);
		}
	}

	@SuppressWarnings("unchecked")
	private JClassValidator getClassValidator(Object value) {
		Class clazz = value.getClass();
		JClassValidator validator = childClassValidators.get(reflectionManager
				.toXClass(clazz));
		if (validator == null) { // handles polymorphism
			validator = new JClassValidator(clazz);
		}
		return validator;
	}

	/**
	 * Apply constraints of a particular property on a bean instance and return
	 * all the failures. Note this is not recursive.
	 */
	// TODO should it be recursive?
	public InvalidValue[] getInvalidValues(T bean, String propertyName) {
		List<InvalidValue> results = new ArrayList<InvalidValue>();

		for (int i = 0; i < memberValidators.size(); i++) {
			XMember getter = memberGetters.get(i);
			if (getPropertyName(getter).equals(propertyName)) {
				Object value = getMemberValue(bean, getter);
				Validator validator = memberValidators.get(i);
				if (!validator.isValid(value)) {
					results.add(new InvalidValue(interpolate(validator),
							beanClass, propertyName, value, bean));
				}
			}
		}

		return results.toArray(new InvalidValue[results.size()]);
	}

	/**
	 * Apply constraints of a particular property value of a bean type and
	 * return all the failures. The InvalidValue objects returns return null for
	 * InvalidValue#getBean() and InvalidValue#getRootBean() Note this is not
	 * recursive.
	 */
	// TODO should it be recursive?
	public InvalidValue[] getPotentialInvalidValues(String propertyName,
			Object value) {
		List<InvalidValue> results = new ArrayList<InvalidValue>();

		for (int i = 0; i < memberValidators.size(); i++) {
			XMember getter = memberGetters.get(i);
			if (getPropertyName(getter).equals(propertyName)) {
				Validator validator = memberValidators.get(i);
				if (!validator.isValid(value)) {
					results.add(new InvalidValue(interpolate(validator),
							beanClass, propertyName, value, null));
				}
			}
		}

		return results.toArray(new InvalidValue[results.size()]);
	}

	private Object getMemberValue(T bean, XMember getter) {
		Object value;
		try {
			value = getter.invoke(bean);
		} catch (Exception e) {
			throw new IllegalStateException("Could not get property value", e);
		}
		return value;
	}

	public String getPropertyName(XMember member) {
		// Do no try to cache the result in a map, it's actually much slower
		// (2.x time)
		String propertyName;
		if (XProperty.class.isAssignableFrom(member.getClass())) {
			propertyName = member.getName();
		} else if (XMethod.class.isAssignableFrom(member.getClass())) {
			propertyName = member.getName();
			if (propertyName.startsWith("is")) {
				propertyName = Introspector.decapitalize(propertyName
						.substring(2));
			} else if (propertyName.startsWith("get")) {
				propertyName = Introspector.decapitalize(propertyName
						.substring(3));
			}
			// do nothing for non getter method, in case someone want to
			// validate a PO Method
		} else {
			throw new AssertionFailure("Unexpected member: "
					+ member.getClass().getName());
		}
		return propertyName;
	}

	/** @deprecated */
	private String replace(String message, Annotation parameters) {
		StringTokenizer tokens = new StringTokenizer(message, "#{}", true);
		StringBuilder buf = new StringBuilder(30);
		boolean escaped = false;
		boolean el = false;
		while (tokens.hasMoreTokens()) {
			String token = tokens.nextToken();
			if (!escaped && "#".equals(token)) {
				el = true;
			}
			if (!el && "{".equals(token)) {
				escaped = true;
			} else if (escaped && "}".equals(token)) {
				escaped = false;
			} else if (!escaped) {
				if ("{".equals(token))
					el = false;
				buf.append(token);
			} else {
				Method member;
				try {
					member = parameters.getClass().getMethod(token,
							(Class[]) null);
				} catch (NoSuchMethodException nsfme) {
					member = null;
				}
				if (member != null) {
					try {
						buf.append(member.invoke(parameters));
					} catch (Exception e) {
						throw new IllegalArgumentException(
								"could not render message", e);
					}
				} else {
					String string = null;
					try {
						string = messageBundle != null ? messageBundle
								.getString(token) : null;
					} catch (MissingResourceException e) {
						// give a second chance with the default resource bundle
					}
					if (string == null) {
						try {
							string = defaultMessageBundle.getString(token);
						} catch (MissingResourceException e) {
							throw new MissingResourceException(
									"Can't find resource in validator bundles, key "
											+ token, defaultMessageBundle
											.getClass().getName(), token);
						}
					}
					if (string != null)
						buf.append(replace(string, parameters));
				}
			}
		}
		return buf.toString();
	}

	/**
	 * apply the registred constraints rules on the hibernate metadata (to be
	 * applied on DB schema...)
	 * 
	 * @param persistentClass
	 *            hibernate metadata
	 */
	public void apply(PersistentClass persistentClass) {

		for (Validator validator : beanValidators) {
			if (validator instanceof PersistentClassConstraint) {
				((PersistentClassConstraint) validator).apply(persistentClass);
			}
		}

		Iterator<Validator> validators = memberValidators.iterator();
		Iterator<XMember> getters = memberGetters.iterator();
		while (validators.hasNext()) {
			Validator validator = validators.next();
			String propertyName = getPropertyName(getters.next());
			if (validator instanceof PropertyConstraint) {
				try {
					Property property = BinderHelper.findPropertyByName(
							persistentClass, propertyName);
					if (property != null) {
						((PropertyConstraint) validator).apply(property);
					}
				} catch (MappingException pnfe) {
					// do nothing
				}
			}
		}

	}

	public void assertValid(T bean) {
		InvalidValue[] values = getInvalidValues(bean);
		if (values.length > 0) {
			throw new InvalidStateException(values);
		}
	}

	private void writeObject(ObjectOutputStream oos) throws IOException {
		ResourceBundle rb = messageBundle;
		MessageInterpolator interpolator = this.userInterpolator;
		if (rb != null && !(rb instanceof Serializable)) {
			messageBundle = null;
			if (!isUserProvidedResourceBundle) {
				log
						.warn("Serializing a ClassValidator with a non serializable ResourceBundle: ResourceBundle ignored");
			}
		}
		if (interpolator != null && !(interpolator instanceof Serializable)) {
			userInterpolator = null;
			log.warn("Serializing a non serializable MessageInterpolator");
		}
		oos.defaultWriteObject();
		oos.writeObject(messageBundle);
		oos.writeObject(userInterpolator);
		messageBundle = rb;
		userInterpolator = interpolator;
	}

	private void readObject(ObjectInputStream ois) throws IOException,
			ClassNotFoundException {
		ois.defaultReadObject();
		ResourceBundle rb = (ResourceBundle) ois.readObject();
		if (rb == null)
			rb = getDefaultResourceBundle();
		this.messageBundle = rb;
		this.userInterpolator = (MessageInterpolator) ois.readObject();
		this.defaultMessageBundle = ResourceBundle
				.getBundle(DEFAULT_VALIDATOR_MESSAGE);
		reflectionManager = new JavaXFactory();
		initValidator(reflectionManager.toXClass(beanClass),
				new HashMap<XClass, JClassValidator>());
	}
}
